/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of the CometMI Project.
 */
package org.cometmi.gwt.rebind;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import org.cometmi.gwt.client.impl.ObjectWrapperClient;
import org.cometmi.gwt.client.impl.WrapperFactoryClient.InnerWrapperFactory;
import org.cometmi.gwt.shared.MiObject;
import org.cometmi.gwt.shared.MiProxy;
import org.cometmi.gwt.shared.annotations.Protected;
import org.cometmi.gwt.shared.annotations.TargetMiObjectClass;
import org.cometmi.gwt.shared.request.ptypes.Param;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

class WrapperFactoryCreator
{
	/** Simple name of class to be generated */
	private String generatedClassName = null;

	/** Package name of class to be generated */
	private String packageName = null;

	/** The type of provided interface */
	private JClassType providedType;

	private ArrayList<WrapperCreator> wrapperCreators;

	WrapperFactoryCreator()
	{}

	/**
	 * Creates a factory class which by a provided class implementing
	 * {@link MiObject} will return a wrapper to an object of that provided
	 * type.
	 * 
	 * @param logger
	 * @param context
	 * @param typeName
	 * @return
	 * @throws UnableToCompleteException
	 */
	String create(TreeLogger logger, GeneratorContext context, String typeName) throws UnableToCompleteException
	{
		this.wrapperCreators = new ArrayList<WrapperCreator>();

		TypeOracle typeOracle = context.getTypeOracle();

		try
		{
			this.providedType = typeOracle.getType(typeName);
		}
		catch (NotFoundException e)
		{
			logger.log(TreeLogger.ERROR, "Provided class '" + typeName + "' not found", e);
			throw new UnableToCompleteException();
		}

		this.generatedClassName = this.providedType.getSimpleSourceName() + "___Impl";
		this.packageName = this.providedType.getPackage().getName();

		String canonicalName = this.packageName + "." + this.generatedClassName;

		try
		{
			JClassType objectType = typeOracle.getType(MiObject.class.getCanonicalName());
			JClassType proxyType = typeOracle.getType(MiProxy.class.getCanonicalName());

			/* Generating wrapper classes by wrapper creator. */
			for (JClassType pt : proxyType.getSubtypes())
			{
				/* Could be a protected proxy, in such case nothing will be done */
				if (pt.getAnnotation(Protected.class) != null) continue;

				TargetMiObjectClass anno = pt.getAnnotation(TargetMiObjectClass.class);

				if (anno != null)
				{
					JClassType ot = typeOracle.getType(anno.miObjectClass().getCanonicalName());

					if (ot.isAssignableTo(objectType))
						this.wrapperCreators.add(new WrapperCreator(logger, context, ot));
					else
					{
						logger.log(TreeLogger.ERROR, "Provided proxy type '" + pt.getQualifiedSourceName()
								+ "' annotation " + TargetMiObjectClass.class.getName()
								+ " doesn't specify a target object of type " + MiObject.class.getCanonicalName());
						throw new UnableToCompleteException();
					}
				}
			}
		}
		catch (NotFoundException e)
		{
			logger.log(TreeLogger.INFO, "Impossible to get subtypes of  " + MiObject.class.getCanonicalName());
			throw new UnableToCompleteException();
		}

		generateClass(logger, context, typeOracle);

		logger.log(TreeLogger.INFO, "Generated class " + canonicalName);

		return canonicalName;
	}

	private void generateClass(TreeLogger logger, GeneratorContext context, TypeOracle typeOracle)
			throws UnableToCompleteException
	{
		PrintWriter printWriter = context.tryCreate(logger, this.packageName, this.generatedClassName);

		/*
		 * When the print writer is null, the source code has ALREADY been
		 * generated, in such case nothing will be done.
		 */
		if (printWriter == null) return;

		/* initialize composer, set class properties, create source writer */
		ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(this.packageName,
				this.generatedClassName);

		composer.addImport(ObjectWrapperClient.class.getCanonicalName());
		composer.addImport(List.class.getCanonicalName());
		composer.addImport(Param.class.getCanonicalName());

		composer.setSuperclass(this.providedType.getQualifiedSourceName());

		for (WrapperCreator wrCreator : this.wrapperCreators)
		{
			JClassType mainType = wrCreator.getProvideType();
			composer.addImport(mainType.getQualifiedSourceName());

			for (JClassType subType : mainType.getSubtypes())
				composer.addImport(subType.getQualifiedSourceName());
		}

		SourceWriter sourceWriter = null;
		sourceWriter = composer.createSourceWriter(context, printWriter);

		/* Generating constructor and required methods */
		generateConstructor(logger, context, sourceWriter, typeOracle);

		/* close generated class */
		sourceWriter.outdent();
		sourceWriter.println("}");

		context.commit(logger, printWriter);
	}

	/**
	 * Generate source code for the default constructor.
	 * 
	 * @param logger
	 * @param context The generator context
	 * @param w Source writer to output source code.
	 * @param typeOracle
	 * @throws UnableToCompleteException
	 * @throws NotFoundException
	 */
	private void generateConstructor(TreeLogger logger, GeneratorContext context, SourceWriter w, TypeOracle typeOracle)
			throws UnableToCompleteException
	{
		w.println("public " + this.generatedClassName + "() {");
		w.indent();

		w.println("super();");

		for (WrapperCreator wrCreator : this.wrapperCreators)
		{
			JClassType oType = wrCreator.getProvideType();

			addInvoker(logger, context, w, wrCreator, oType.getSimpleSourceName());

			for (JClassType subType : oType.getSubtypes())
				addInvoker(logger, context, w, wrCreator, subType.getSimpleSourceName());
		}

		w.outdent();
		w.println("}");
	}

	private void addInvoker(TreeLogger logger, GeneratorContext context, SourceWriter w, WrapperCreator wrCreator,
			String subClassName) throws UnableToCompleteException
	{
		w.println();

		logger.log(TreeLogger.INFO, "Adding to the factory " + subClassName);

		w.print("super.wrappers.put(%s.class.getName(), ", subClassName);
		w.println("new %s(){", InnerWrapperFactory.class.getCanonicalName());
		w.indent();

		w.println("public %s create() {", ObjectWrapperClient.class.getCanonicalName());
		w.indent();

		w.print("return new %s();", wrCreator.getCanonicalname());

		w.outdent();
		w.println("}");

		w.println();

		w.println("public %s create(String signature, Object[] args) throws Exception {",
				ObjectWrapperClient.class.getSimpleName());
		w.indent();

		w.println("%s wrapper = new %s();", wrCreator.getCanonicalname(), wrCreator.getCanonicalname());
		w.println("wrapper.setInstance(%s.%s(signature, args));", wrCreator.getCanonicalname(),
				wrCreator.getStaticCreatorMethodName());

		w.println("return wrapper;");

		w.outdent();
		w.println("}");

		w.outdent();
		w.println("});");
	}
}
